# Copyright (c) HySoP 2011-2024
#
# This file is part of HySoP software.
# See "https://particle_methods.gricad-pages.univ-grenoble-alpes.fr/hysop-doc/"
# for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections.abc import Iterable
import numpy as np
[docs]
class InstanceOf:
def __init__(self, cls):
assert isinstance(cls, type)
self.cls = cls
[docs]
def match_instance(self, obj):
return isinstance(obj, self.cls)
def __str__(self):
return f"InstanceOf({self.cls.__name__})"
def __repr__(self):
return f"InstanceOf({self.cls.__name__})"
[docs]
def check_instance(val, cls, allow_none=False, check_kwds=True, **kargs):
"""
Raise a TypeError if val is not an instance of cls.
cls can be a tuple of types like isinstance(...) capabilities.
If allow_none is False and val is None, an exception is raised.
This function is specialized for some types like array like and dictionaries.
For array likes and dicts an additional values=val_cls parameter can be
passed to check contained values type.
For dictionnaries an additional keys=key_cls parameter can be
passed to check contained keys type in addition to values.
val_cls and key_cls have the same format as cls (ie. can be a tuple of valid types).
For iterables you can check size, minsize, maxsize, min_value and max_value using keys:
'size', 'minsize', 'maxsize', 'minval', 'maxval'.
For np.ndarrays, in addition to the iterables keys you can check ndim, shape and dtype
using the keys 'ndim', 'shape' and 'dtype'.
If shape and size are specified at the same time they should be compatible.
If python builtin variable __debug__ is set to True
(ie. python was run with -O option or PYTHONOPTIMIZE
environment variable was set to a non-empty string),
the check is disabled (like asserts) and this method
returns immediately.
"""
if not __debug__:
return
if "allow_None" in kargs:
msg = "Unknown keyword argument 'allow_None', did you mean allow_none ?"
raise RuntimeError(msg)
allcls = to_tuple(cls)
if val is None:
if allow_none:
return
elif type(None) in allcls:
return
else:
raise ValueError("value cannot be None.")
def print_offending_value(val, all_cls):
try:
types = "{}[{}{}]".format(
type(val).__name__,
", ".join([str(type(v)) for v in val[:5]]),
", ..." if (len(val) > 5) else "",
)
except:
types = type(val)
msg = "\nFATAL ERROR: Type did not match any of types {} for the following value:\n{}\n"
msg += "which is of type {}.\n"
msg = msg.format(all_cls, val, types)
print(msg)
if not any(isinstance(val, cls) for cls in allcls):
msg = "Expected an instance of {} but got a value of type {}."
msg = msg.format(cls, type(val))
print_offending_value(val, allcls)
raise TypeError(msg)
if type(val) in (list, tuple, set, frozenset):
size = kargs.pop("size", None)
minsize = kargs.pop("minsize", None)
maxsize = kargs.pop("maxsize", None)
unique = kargs.pop("unique", None)
minval = kargs.pop("minval", None)
maxval = kargs.pop("maxval", None)
if "values" in kargs:
all_val_cls = to_tuple(kargs.pop("values"))
else:
all_val_cls = None
if size and len(val) != size:
msg = "Size of {} is {} and does not match required size {}."
msg = msg.format(type(val), len(val), size)
raise ValueError(msg)
if minsize and len(val) < minsize:
msg = "Size of {} is {} and does not match required minsize {}."
msg = msg.format(type(val), len(val), minsize)
raise ValueError(msg)
if maxsize and len(val) > maxsize:
msg = "Size of {} is {} and does not match required maxsize {}."
msg = msg.format(type(val), len(val), maxsize)
raise ValueError(msg)
if (unique is True) and len(set(val)) != len(val):
msg = "Given {} does not contain unique values."
msg = msg.format(type(val))
raise ValueError(msg)
if (minval is not None) or (maxval is not None) or (all_val_cls is not None):
for v in val:
if (all_val_cls is not None) and not any(
isinstance(v, val_cls) for val_cls in all_val_cls
):
msg = "Value contained in given {} is not an instance of {} but a "
msg += "value of type {}."
msg = msg.format(type(val).__name__, all_val_cls, type(v))
print_offending_value(v, all_val_cls)
raise TypeError(msg)
if (minval is not None) and (v < minval):
msg = "Value contained in given {} has value {} which is less "
msg += "than the specified minimum value {}."
msg = msg.format(cls.__name__, v, minval)
raise ValueError(msg)
if (maxval is not None) and (v > maxval):
msg = "Value contained in given {} has value {} which is greater "
msg += "than the specified maximum value {}."
msg = msg.format(cls.__name__, v, maxval)
raise ValueError(msg)
elif isinstance(val, dict):
size = kargs.pop("size", None)
minsize = kargs.pop("minsize", None)
maxsize = kargs.pop("maxsize", None)
unique = kargs.pop("unique", None)
if size and len(val.keys()) != size:
msg = "Size of {} is {} and does not match required size {}."
msg = msg.format(dict, len(val.keys()), size)
raise ValueError(msg)
if minsize and len(val.keys()) < minsize:
msg = "Size of {} is {} and does not match required minsize {}."
msg = msg.format(dict, len(val.keys()), minsize)
raise ValueError(msg)
if maxsize and len(val.keys()) > maxsize:
msg = "Size of {} is {} and does not match required maxsize {}."
msg = msg.format(dict, len(val.keys()), maxsize)
raise ValueError(msg)
if (unique is True) and len(set(val.values())) != len(val):
msg = "Given {} does not contain unique values."
msg = msg.format(type(val))
raise ValueError(msg)
if "keys" in kargs:
all_key_cls = to_tuple(kargs.pop("keys"))
for k in val.keys():
if not any(isinstance(k, key_cls) for key_cls in all_key_cls):
msg = "Key contained in {} is not an instance of {} but a value of type {}."
msg = msg.format(cls.__name__, all_key_cls, type(k))
print_offending_value(k, all_key_cls)
raise TypeError(msg)
if "values" in kargs:
all_val_cls = to_tuple(kargs.pop("values"))
for k, v in val.items():
if hasattr(k, "name"):
key = k.name
elif hasattr(k, "id"):
key = k.id
else:
key = id(k)
if not any(isinstance(v, val_cls) for val_cls in all_val_cls):
msg = "Value contained in {} at key '{}' is not an instance of {}, got {}."
msg = msg.format(cls.__name__, key, all_val_cls, type(v))
print_offending_value(v, all_val_cls)
raise TypeError(msg)
elif isinstance(val, np.ndarray):
from hysop.tools.misc import prod
dtype = kargs.pop("dtype", None)
shape = kargs.pop("shape", None)
size = kargs.pop("size", None)
ndim = kargs.pop("ndim", None)
minsize = kargs.pop("minsize", None)
maxsize = kargs.pop("maxsize", None)
minval = kargs.pop("minval", None)
maxval = kargs.pop("maxval", None)
unique = kargs.pop("unique", None)
all_val_cls = kargs.pop("values", None)
if shape and size:
assert prod(shape) == size
if dtype and (dtype != val.dtype):
msg = "np.ndarray dtype does not match, expected {} but got {}."
msg = msg.format(dtype, val.dtype)
raise ValueError(msg)
if ndim and (ndim != val.ndim):
msg = "np.ndarray ndim does not match, expected {} but got {}."
msg = msg.format(ndim, val.ndim)
raise ValueError(msg)
if size and (size != val.size):
msg = "np.ndarray size does not match, expected {} but got {}."
msg = msg.format(size, val.size)
raise ValueError(msg)
if minsize and val.size < minsize:
msg = "np.ndarray size is {} and does not match required minsize {}."
msg = msg.format(val.size, minsize)
raise ValueError(msg)
if maxsize and val.size > maxsize:
msg = "np.ndarray size is {} and does not match required maxsize {}."
msg = msg.format(val.size, maxsize)
raise ValueError(msg)
if shape and (shape != val.shape):
msg = "np.ndarray shape does not match, expected {} but got {}."
msg = msg.format(shape, val.shape)
raise ValueError(msg)
if minval and (val < minval).any():
msg = "np.ndarray min value constraint failed, minimum was set to {} but got {}."
msg = msg.format(minval, val.min())
raise ValueError(msg)
if maxval and (val > maxval).any():
msg = "np.ndarray max value constraint failed, maximum was set to {} but got {}."
msg = msg.format(maxval, val.max())
raise ValueError(msg)
if (unique is True) and len(set(val.tolist())) != val.size:
msg = "Given {} does not contain unique values."
msg = msg.format(type(val))
raise ValueError(msg)
if all_val_cls:
assert val.size < 32, "Do not check instance type on big numpy arrays."
for v in val.ravel():
if not any(isinstance(v, val_cls) for val_cls in to_tuple(all_val_cls)):
msg = "Value contained in given np.ndarray is not an instance "
msg += "of {} but a value of type {}."
msg = msg.format(all_val_cls, type(v))
print_offending_value(v, all_val_cls)
raise TypeError(msg)
else:
minval = kargs.pop("minval", None)
maxval = kargs.pop("maxval", None)
size = kargs.pop("size", None)
if minval and val < minval:
msg = "Value {} is less than the specified minimum value {}."
msg = msg.format(val, minval)
raise ValueError(msg)
if maxval and val > maxval:
msg = "Value {} is greater than the specified maximum value {}."
msg = msg.format(val, maxval)
raise ValueError(msg)
if kargs:
# Ignore unused 'keys' argument if val can be a dict and
# is instance of another type
if not (dict in allcls and type(val) is not dict):
raise RuntimeError(f"Some arguments were not used ({kargs}).")
[docs]
def to_tuple(arg, cast=None):
"""
Convert any of the following types:
scalar,tuple,list,set,frozenset,np.ndarray
to a tuple.
"""
cast = cast or (lambda x: x)
if isinstance(arg, (str, dict, type)):
return (arg,)
elif hasattr(arg, "__tuple__"):
return tuple(cast(x) for x in arg.__tuple__())
elif isinstance(arg, np.ndarray):
try:
return tuple(cast(x) for x in arg.tolist())
except:
return (cast(arg),)
elif isinstance(arg, (tuple, list, frozenset, set, Iterable)):
return tuple(cast(x) for x in arg)
else:
return (cast(arg),)
[docs]
def to_list(arg, cast=None):
"""
Convert any of the following types:
scalar,tuple,list,set,frozenset,np.ndarray
to a list.
"""
return list(to_tuple(arg, cast=cast))
[docs]
def to_set(arg, cast=None):
"""
Convert any of the following types:
scalar,tuple,list,set,frozenset,np.ndarray
to a set.
"""
return set(to_tuple(arg, cast=cast))
[docs]
def to_frozenset(arg, cast=None):
"""
Convert any of the following types:
scalar,tuple,list,set,frozenset,np.ndarray
to a frozenset.
"""
return frozenset(to_tuple(arg, cast=cast))
[docs]
def extend_array(value, dim, dtype=None, fillval=None):
"""
Extend value to have specified dimension.
If fillval is None, the last value is used to complete the array.
Returns a np.ndarray of specified dtype and size dim.
"""
value = to_list(value)
assert len(value) <= dim
if fillval is None:
fillval = value[-1]
value = np.asarray(value + [fillval] * (dim - len(value)), dtype=dtype)
return value
[docs]
def first_not_None(a0, *args):
"""
Return the first argument that is not None.
If only one argument is given it is considered as an iterable,
else iterates on (a0,)+args.
If all arguments are None, return None.
"""
if len(args) == 0:
try:
return next((a for a in a0 if a is not None), None)
except TypeError: # a0 is not iterable
return a0
else:
return next((a for a in (a0,) + args if a is not None), None)